﻿///////////////////////////////////////////////////////////////////////////////
//
// This file is part of the BassDrop project
//   https://code.google.com/p/bass-drop/
//
//
// Creator:	Gergely Krajcsovszki (krajcsovszkig@hotmail.com)
// Date:	2013-08-04 17:10
//
//
// Copyright © 2013-2014. Gergely Krajcsovszki
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BassDrop.Core.Common.Services;

namespace BassDrop.Core.Common.Plugins
{
	/// <summary>
	/// Base class of the main class of BassDrop plugins.
	/// It declares various properties providing information about the plugin.
	/// </summary>
	public abstract class BassDropPluginBase : IDisposable, INotifyPropertyChanged
	{
		private object m_lock = new object();

		/// <summary>
		/// The instances created so far of the plugin.
		/// The keys are the names of each instance.
		/// </summary>
		private readonly Dictionary<string, PluginObjectBase> m_instances = new Dictionary<string, PluginObjectBase>();

		#region Properties

		/// <summary>
		/// The type of the plugin
		/// </summary>
		public PluginType PluginType
		{
			get;
			protected set;
		}

		private string m_name;

		/// <summary>
		/// The name of the plugin
		/// </summary>
		public string Name
		{
			get
			{
				return m_name;
			}

			protected set
			{
				if ( m_name != value )
				{
					m_name = value;
					OnPropertyChanged( "Name" );
				}
			}
		}

		private string m_shortDescription;

		/// <summary>
		/// A maximum 50 character long description of the plugin
		/// </summary>
		public string ShortDescription
		{
			get
			{
				return m_shortDescription;
			}

			protected set
			{
				if ( m_shortDescription != value )
				{
					m_shortDescription = value;
					OnPropertyChanged( "ShortDescription" );
				}
			}
		}

		private string m_longDescription;

		/// <summary>
		/// A full description of the plugin. (Length practically unlimited)
		/// </summary>
		public string LongDescription
		{
			get
			{
				return m_longDescription;
			}

			protected set
			{
				if ( m_longDescription != value )
				{
					m_longDescription = value;
					OnPropertyChanged( "LongDescription" );
				}
			}
		}

		/// <summary>
		/// The icon of the plugin. It will be displayed at certain places. Should be 128×128 px.
		/// The property gives access to the file in a stream
		/// </summary>
		public Stream Icon
		{
			get;
			protected set;
		}

		/// <summary>
		/// The revision number of the plugin. Will be used to determine which is newer of two versions.
		/// </summary>
		public int Revision
		{
			get;
			protected set;
		}

		/// <summary>
		/// A pretty version string of the plugin. Will be presented to the user.
		/// Suggested format: Major.Minor.Build:Revision
		/// </summary>
		public string Version
		{
			get;
			protected set;
		}

		/// <summary>
		/// List of protocols supported by this plugin.
		/// 
		/// It can mean playable media formats, usable cloud providers, etc.
		/// Formats need to be file extensions with a leading dot.
		/// 
		/// The names are case insensitive
		/// </summary>
		public List<string> SupportedProtocols
		{
			get;
			protected set;
		}

		/// <summary>
		/// The controller object for the application running the plugin
		/// </summary>
		public Controller AppController
		{
			get;
			private set;
		}

		/// <summary>
		/// The path of the directory where the plugin can store its stuff
		/// </summary>
		public string PrivateDirectory
		{
			get;
			private set;
		}

		private PluginOptionsSetBase m_options;

		/// <summary>
		/// Stores the global (not instance-specific) options of the plugin
		/// </summary>
		public PluginOptionsSetBase Options
		{
			get
			{
				return m_options;
			}

			protected set
			{
				if ( value != m_options )
				{
					if ( m_options != null )
					{
						m_options.PropertyChanged -= m_options_PropertyChanged;
					}

					m_options = value;

					if ( m_options != null )
					{
						m_options.PropertyChanged -= m_options_PropertyChanged;
						m_options.PropertyChanged += m_options_PropertyChanged;
					}

					OnSettingsChanged( m_options, new PropertyChangedEventArgs( "Options" ) );
					OnPropertyChanged( "Options" );
				}
			}
		}

		#endregion Properties

		#region Events

		/// <summary>
		/// Event fired when an instance is added or deleted
		/// </summary>
		public event EventHandler InstancesChanged;

		/// <summary>
		/// Event fired when the settings of the plugin or its instances change
		/// </summary>
		public event PropertyChangedEventHandler SettingsChanged;

		/// <summary>
		/// Event fired when one of the properties of this object changes
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged;

		/// <summary>
		/// Fire a new InstancesChanged event
		/// </summary>
		/// <param name="e"></param>
		private void OnInstancesChanged( EventArgs e )
		{
			if ( InstancesChanged != null )
			{
				InstancesChanged( this, e );
			}
		}

		/// <summary>
		/// Fire a new SettingsChanged event
		/// </summary>
		/// <param name="settings"></param>
		/// <param name="e"></param>
		private void OnSettingsChanged( PluginOptionsSetBase settings, PropertyChangedEventArgs e )
		{
			if ( SettingsChanged != null )
			{
				SettingsChanged( settings, e );
			}
		}

		/// <summary>
		/// Fires a new PropertyChanged event
		/// </summary>
		/// <param name="propertyName">the name of the property that have changed</param>
		protected virtual void OnPropertyChanged( string propertyName )
		{
			if ( PropertyChanged != null )
			{
				PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
			}
		}

		#endregion Events

		/// <summary>
		/// Sets up the base object
		/// </summary>
		/// <param name="appController">The controller object for the application running the plugin</param>
		/// <param name="pluginPrivateDir">The path of the directory where the plugin can store its stuff</param>
		protected BassDropPluginBase( Controller appController, string pluginPrivateDir )
		{
			AppController = appController;
			PrivateDirectory = pluginPrivateDir;
			SupportedProtocols = new List<string>();

			AppController.ApplicationLanguageChanged += AppController_ApplicationLanguageChanged;
		}

		/// <summary>
		/// Sets the language used by the plugin to the specified one
		/// </summary>
		/// <param name="language">the 2 letter code of the language, eg. en</param>
		public abstract void SetLanguage( string language );

		/// <summary>
		///  Creates a new instance of the plugin with the specified name and loads its settings. Saves the created instance.
		/// If an instance with the passed name already exists, returns it and doesn't create a new one.
		/// </summary>
		/// <param name="name">the name of the new instance or null to create a temporary one</param>
		/// <param name="settings">the settings object, from where the settings of the new instance can be loaded if they exist</param>
		/// <returns></returns>
		public async Task<PluginObjectBase> GetOrCreateInstance( string name, ISettings settings = null )
		{
			try
			{
				Monitor.Enter( m_lock ); // Cannot use lock block because of the await

				PluginObjectBase instance = null;

				if ( name != null )
				{
					instance = GetNamedInstance( name );
				}

				if ( instance == null )
				{
					instance = await DoCreateInstance( name ?? "__temp", settings );

					if ( name != null )
					{
						m_instances.Add( name, instance );
						instance.SettingsChanged += instance_SettingsChanged;
						instance.StateChanged += instance_StateChanged;
						OnInstancesChanged( EventArgs.Empty );
					}
				}

				return instance;
			}
			finally
			{
				Monitor.Exit( m_lock );
			}
		}

		/// <summary>
		/// Creates a new instance of the plugin with the specified name and loads its settings.
		/// </summary>
		/// <param name="name">the name of the new instance or null to create a temporary one</param>
		/// <param name="settings">the settings object, from where the settings of the new instance can be loaded if they exist</param>
		/// <returns></returns>
		protected abstract Task<PluginObjectBase> DoCreateInstance( string name, ISettings settings );

		/// <summary>
		/// Returns the instance of the main object of the plugin with the specified name
		/// or null if none exists.
		/// </summary>
		/// <param name="instanceName">
		///		The name of the plugin instance, can be used to differentiate between them
		///		when multiple instances are used, eg. multiple cloud accounts of the same
		///		type are connected
		/// </param>
		/// <returns></returns>
		public PluginObjectBase GetNamedInstance( string instanceName )
		{
			lock ( m_lock )
			{
				if ( m_instances.ContainsKey( instanceName ) )
				{
					return m_instances[instanceName];
				}

				return null;
			}
		}

		/// <summary>
		/// Returns the default instance of the main object of the plugin.
		/// </summary>
		/// <returns></returns>
		public async Task<PluginObjectBase> GetStaticInstance()
		{
			try
			{
				Monitor.Enter( m_lock ); // Cannot use lock block because of the await

				return GetNamedInstance( "__static" ) ?? await GetOrCreateInstance( "__static" );
			}
			finally
			{
				Monitor.Exit( m_lock );
			}
		}

		/// <summary>
		/// Returns a new instance of the main object of the plugin.
		/// </summary>
		/// <returns></returns>
		public async Task<PluginObjectBase> GetTemporaryInstance()
		{
			return await GetOrCreateInstance( null );
		}

		/// <summary>
		/// Removes and disposes of the plugin instance with the passed name
		/// </summary>
		/// <param name="name"></param>
		public void RemoveNamedInstance( string name )
		{
			if ( m_instances.ContainsKey( name ) )
			{
				IDisposable instance = m_instances[name] as IDisposable;

				m_instances.Remove( name );

				if ( instance != null )
				{
					instance.Dispose();
				}

				OnInstancesChanged( EventArgs.Empty );
			}
		}

		/// <summary>
		/// Returns the reusable instances of this plugin
		/// </summary>
		/// <returns></returns>
		public IEnumerable<PluginObjectBase> GetInstances()
		{
			lock ( m_lock )
			{
				return from instance
					   in m_instances.Values
					   where !instance.InstanceName.StartsWith( "__" )
					   select instance;
			}
		}

		/// <summary>
		/// Loads the plugin settings and creates the saved instances
		/// </summary>
		/// <param name="settings">the seettings object to load from</param>
		public async Task Load( ISettings settings )
		{
			if ( Options != null )
			{
				await Options.Load( settings );
			}

			foreach ( var instance in settings.PluginInstances( Name ) )
			{
				await GetOrCreateInstance( instance, settings );
			}
		}

		/// <summary>
		/// Saves the plugin settings and its instances
		/// </summary>
		/// <param name="settings">the seettings object to save to</param>
		public void Save( ISettings settings )
		{
			settings.ClearPluginSettings( Name );

			if ( Options != null )
			{
				Options.Save( settings );
			}

			foreach ( var instance in GetInstances() )
			{
				instance.Save( settings );
			}
		}

		#region IDisposable Members

		/// <summary>
		/// Disposes the objects created by the plugin
		/// </summary>
		public virtual void Dispose()
		{
			foreach ( var item in m_instances.Values )
			{
				var instance = item as IDisposable;

				if ( instance != null )
				{
					instance.Dispose();
				}
			}

			m_instances.Clear();

			if ( Icon != null )
			{
				Icon.Dispose();
				Icon = null;
			}
		}

		#endregion

		private void m_options_PropertyChanged( object sender, PropertyChangedEventArgs e )
		{
			OnSettingsChanged( (PluginOptionsSetBase) sender, e );
		}

		private void instance_SettingsChanged( object sender, PropertyChangedEventArgs e )
		{
			OnSettingsChanged( (PluginOptionsSetBase) sender, e );
		}

		private void instance_StateChanged( object sender, PropertyChangedEventArgs e )
		{
			OnInstancesChanged( EventArgs.Empty );
		}

		private void AppController_ApplicationLanguageChanged( object sender, PropertyChangedEventArgs e )
		{
			SetLanguage( AppController.ApplicationLanguage.LanguageCode );
		}
	}
}
